/* $Id: central.cc jjs $ */
/*
   Copyright (C) 2007 by Jurjen Stellingwerff <jurjen@stwerff.xs4all.nl>
   Part of the Moros Project http://moros.sourceforge.net/

   This program is free software; you can redistribute it and/or modify
   it under the terms of the GNU General Public License version 3.
   This program is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY.

   See the COPYING file for more details.
*/

#define DEBUG_ROUTINE "central"

#include <sys/stat.h>
#include <sys/types.h>
#include <time.h>
#include <iostream>
#include <iomanip>
#include <sstream>
#include <stdlib.h>
#include <errno.h>
#include "gcrypt.h"
#include "signal.h"
#include "game.h"
#include "user.h"
#include "../general/log.h"
#include "../general/communicate.h"
#include "../general/misc.h"
#include "../general/arguments.h"
#include "../general/util.h"
#include "central.h"

/** \dir src/central
  * The moros central server.
  * Used to store user identification and different games to join.
  */


/// General communication handler with the moros client

central::central() {
    current_user=0;
    timestamp=0;
    game_dir="/var/lib/moros";
    current_time=time(NULL);
}


void central::config_handler(channel *ch) {
    if (ch->key!="game_number")
        dbg_fatal("Expecting game_number in data file found: %s", ch->key.c_str());
    game_number=atol(ch->value.c_str());
    ch->next();
    while (true) {
    	long g=load_game(ch);
    	if (g>0) {
			dbg_calls("Loaded game %li", g);
    		game ng=get_game(g);
    		ng.timestamp=current_time;
    		games[g]=ng;
        }
        load_user(ch);
        if (!ch->next()) break;
    }
    ch->finish();
}

/// Save all the data to files for backup

void central::save_all(std::string config_file) {
    dbg_data("Save all");
    map<string, user>::iterator u;
    map<long, game>::iterator g;
    channel *ch=comm.writing(config_file+".new");
    ch->write("game_number", game_number);
    for (g=games.begin(); g!=games.end(); g++) g->second.save(ch);
    for (u=users.begin(); u!=users.end(); u++) {
        if (u->first != "" && u->second.username != "") u->second.save(ch);
    }
    ch->finish();
    rename(config_file.c_str(), (config_file+".old").c_str());
    rename((config_file+".new").c_str(), config_file.c_str());
}

/// Communicate with the client by a TLS secure connection

/** \comm <b>new_user</b> &lt;username> Create a new user record
- follow up
  - <b>password</b>: &lt;password>\n
    needed password
- responses
  - error: existing user name\n
    when another user with this name already exists
  - error: expecting password\n
    when a password was not provided
  - <b>session</b>: &lt;session-id>\n
    on succesfull login
 \comm <b>user</b> &lt;username> Login with a current user
- follow up
  - <b>password</b>: &lt;password>\n
    needed password
- responses
  - error: expecting user name\n
    no user or new_user command was found
  - error: expecting password\n
    no password was found
  - error: unauthorized entry\n
    no user record with this password was found
  - <b>session</b>: &lt;session-id>\n
    on succesfull login
*/

std::string central::rand_session() {
  	std::ostringstream streamOut;
   	streamOut.fill('0');
    streamOut << setw(10) << random() << setw(10) << random();
    return streamOut.str();
}

void central::tls_handler(channel *ch) {
    if (ch->key=="new_user" && ch->value!="") {
        user u;
        if (users.count(ch->value)>0) {
            ch->write("error", "existing user name");
            ch->send();
            return;
        }
        u.username=ch->value;
        u.ch=ch;
        ch->next();
        if (ch->key!="password" || ch->value=="") {
            dbg_error("Expecting password but found %s with %s", ch->key.c_str(), ch->value.c_str());
            ch->write("error", "expecting password");
            ch->send();
            return;
        }
        u.session=rand_session();
        u.sessnr=1;
        ch->write("session", u.session);
        ch->send();
        u.password=hash(ch->value);
        users[u.username]=u;
        if (!ch->next()) return;
    } else {
        if (ch->key!="user" || ch->value=="") {
            dbg_error("Expecting user but found %s with %s", ch->key.c_str(), ch->value.c_str());
            ch->write("error", "expecting user name");
            ch->send();
            return;
        }
        user &u=users[ch->value];
        ch->next();
        if (ch->key!="password") {
            dbg_error("Expecting password but found %s with %s", ch->key.c_str(), ch->value.c_str());
            ch->write("error", "expecting password");
            ch->send();
            return;
        }
        if (hash(ch->value) != u.password) {
            ch->write("error", "unauthorized entry");
            ch->send();
            return;
        }
        if (!ch->next()) {
            u.ch=ch;
            u.session=rand_session();
            u.sessnr=1;
            ch->write("session", u.session);
            ch->send();
            return;
        }
        if (ch->key=="new_password") {
            users[u.username].password=ch->value;
            if (!ch->next()) return;
        }
        if (ch->key=="game") {
            if (games.count(tolong(ch->value))==0) {
                ch->write("error", "unknown game number");
                ch->send();
                return;
            }
            game *g=&games[tolong(ch->value)];
            if (g->owner != u.username) {
                ch->write("error", "not an owned game");
                ch->send();
                return;
            }
            ch->next();
            if (ch->key=="state") {
                g->state=toint(ch->value);
            }
            g->session=rand_session();
            ch->write("session", g->session);
            ch->send();
        }
    }
}

/// Create a new game record

/** \comm <b>game</b> Create or change a game record
- follow up
  - <b>name</b>: &lt;name>
  - <b>descr</b>: &lt;description>
  - <b>join</b>: &lt;join method number>
  - <b>goal</b>: &lt;game goal number>
  - <b>map</b>: &lt;map name>
  - <b>url</b>: &lt;game server url>
  - <b>party</b>: &lt;party name>
    - <b>desc</b>: &lt;party description>
- responses
  - <b>gameid</b>: &lt;id>\n
    the reference number of this game on a new game
*/

void central::change_game(channel *ch) { //< Change data on a game
    if (ch->key=="game") {
        long id=atol(ch->value.c_str());
        game g;
        if (id==0) {
            game_number++;
            g.id=game_number;
            ch->write("gameid", g.id);
            ch->send();
            g.owner=current_user->username;
        } else {
            g=games[id];
            if (g.owner != current_user->username) {
                ch->write("error", "Game owned by another user");
                return;
            }
        }
        if (ch->peek("name")) {
            ch->next();
            g.name=ch->value;
        }
        if (ch->peek("descr")) {
            ch->next();
            g.description=ch->value;
        }
        if (ch->peek("join")) {
            ch->next();
            g.join=toint(ch->value);
        }
        if (ch->peek("goal")) {
            ch->next();
            g.goal=toint(ch->value);
        }
        if (ch->peek("map")) {
            ch->next();
            g.map=ch->value;
        }
        if (ch->peek("url")) {
            ch->next();
            g.url=ch->value;
        }
        if (ch->peek("party")) {
            vector<party> parties;
            while (ch->peek("party")) {
                ch->next();
                parties.push_back(load_party(ch));
            }
            g.parties.clear();
            g.parties=parties;
        }
        g.timestamp=current_time;
        games[id]=g;
        if (g.state==state_defining) {
            channel *fch=comm.writing(game_dir+"/"+tostr(g.id)+".data");
            g.server_save(fch);
            fch->finish();
            system(("gzip "+game_dir+"/"+tostr(g.id)+".data").c_str());
        }
        dbg_calls("Wrote game %li", g.id);
    }
}

/// Receive data from the moros client program

void central::client_handler(channel *ch) {
    if (ch->finished) {
        dbg_comm("Finished client connection");
        return;
    }
    if (ch->lock) {
        if (ch->key!="user") {
            dbg_error("Expecting user but found %s with %s", ch->key.c_str(), ch->value.c_str());
            ch->write("error", "expecting user name");
            ch->send();
            ch->finish();
            return;
        }
        std::string usern=ch->value;
        user &u=users[usern];
        ch->next();
        if (u.username != usern) {
            ch->write("error", "session number not valid");
            dbg_state("Unknown user '%s'", usern.c_str());
            ch->send();
            ch->finish();
            return;
        }
        if (ch->key!="session") {
            dbg_error("Expecting session but found %s with %s", ch->key.c_str(), ch->value.c_str());
            ch->write("error", "expecting session number");
            ch->send();
            ch->finish();
            return;
        }
        dbg_calls("User %s session %li", u.username.c_str(), u.sessnr);
        if (derived(u.session, u.sessnr++)!=ch->value) {
            ch->write("error", "session number not valid");
            dbg_state("Bad session number for user '%s'", u.username.c_str());
            ch->send();
            ch->finish();
            return;
        }
        ch->next();
        ch->write("session", derived(u.session, u.sessnr++));
        ch->send();
        current_user=&users[usern];
        ch->payload=current_user;
        ch->lock=false;
    } else {
        if (ch->payload==0) {
            dbg_error("No user information in channel payload");
            return;
        }
        current_user=(user*)ch->payload;
        dbg_calls("Current user: %s", current_user->username.c_str());
    }
    while (true) {
        if (current_user->password=="") { // this is a server connecting
            if (ch->key=="data") {
                long gnr=tolong(current_user->username);
                if (games.count(gnr)>0) ch->fread(game_dir+"/games/"+tostr(gnr)+".data.gz");
                else ch->send();
                ch->next();
                if (ch->key=="map") {
                    if (isfile(game_dir+"/games/"+tostr(gnr)+".map.gz")) {
                        ch->fread(game_dir+"/games/"+tostr(gnr)+".map.gz");
                    } else ch->send();
                }
            }
            if (ch->key=="client") {
                user &client=users[ch->value];
                if (!ch->next()) return;
                if (ch->key=="session" && derived(client.session, client.sessnr++)==ch->value) {
                    ch->write("session", derived(client.session, client.sessnr++));
                    ch->send();
                }
            }
        }
        /**
          \todo limit 1 person to download more than 3 games each day
        */
        if (ch->key=="readgame") { /// client read data from game
            string filen=ch->value;
            if (isfile(game_dir+"/games/"+filen+".data.gz")) {
                ch->fread(game_dir+"/games/"+filen+".data.gz");
            } else ch->send();
            ch->next();
            if (ch->key=="map") {
                if (isfile(game_dir+"/games/"+filen+".map.gz")) {
                    ch->fread(game_dir+"/games/"+filen+".map.gz");
                } else ch->send();
                ch->next();
            }
        }
        if (ch->key=="writegame") { /// client write data from game, must be owner
            if (games[tolong(ch->value)].owner!=current_user->username) {
                ch->write("error", "Not the owner of this game");
            } else {
                string filen=ch->value;
                ch->write("game", "data");
                ch->fwrite(game_dir+"/games/"+filen+".data.gz");
                ch->send();
                ch->write("map", "data");
                ch->fwrite(game_dir+"/games/"+filen+".map.gz");
                ch->send();
            }
        }
        if (ch->key=="timestamp") {
            dbg_calls("Found timestamp %s", ch->value.c_str());
            map<long, game>::iterator g;
            for (g=games.begin(); g!=games.end(); g++) {
            	dbg_calls("Test game stamp %li with %s", g->second.timestamp, ch->value.c_str());
                if (atol(ch->value.c_str())<g->second.timestamp) g->second.save(ch);
            }
            ch->send();
        }
        if (ch->key=="game") {
            change_game(ch);
        }
        if (!ch->next()) break;
    }
}

bool stop=false;

time_t last_save=0;

/// Stop the server gracefully on a normal interrupt signal

void sig_int(int i) {
    dbg_state("received a SIGINT signal, stopping the server");
    stop=true;
}

communicate tls_comm;

/// Write data and reload configuration on a HUP signal

/**
\todo Also write game data to file
\todo Start a new logfile
*/

void sig_reload(int i) {
    dbg_state("reload data after a SIGHUB signal");
    tls_comm.tls_reload();
}

void central::do_fork(std::string file) {
    dbg_calls("do_fork with pid file '%s'", file.c_str());
    int pid=fork();
    if (pid>0) {
        FILE *fp=fopen(file.c_str(), "w");
        if (!fp) dbg_fatal("Error: %s opening pid file: '%s'", strerror(errno), file.c_str());
        fprintf(fp,"%d\n", pid);
        fclose(fp);
        exit(0);
    }
    fclose(stdin);
    fclose(stdout);
    fclose(stderr);
}


void central::chat_session(std::string sess) {
    user chat;
    chat.sessnr=1;
    chat.username="central_chat";
    chat.session=sess;
    chat.password="";
    users["central_chat"]=chat;
    dbg_calls("Username: %s session: %li", chat.username.c_str(), chat.sessnr);
}

void central::config() {
    mkdir(game_dir.c_str(), S_IRWXU);
    config_file=game_dir+"/data";
    comm = communicate();
    sigc::signal<void, channel*> config_signal;
    config_signal.connect(sigc::mem_fun(*this, &central::config_handler));
    comm.reading(config_file, config_signal);
    comm.complete(10);
}

void central::main_loop() {
    unsigned int nr;
    gcry_randomize (&nr, 4, GCRY_STRONG_RANDOM);
    srandom(time(0));
    dbg_calls("Random init %i", nr);

    tls_comm = communicate();
    tls_comm.set_dir(game_dir);
    sigc::signal<void, channel*> tls_signal;
    tls_signal.connect(sigc::mem_fun(*this, &central::tls_handler));
    tls_comm.tls_server(12001, tls_signal);

    sigc::signal<void, channel*> client_signal;
    client_signal.connect(sigc::mem_fun(*this, &central::client_handler));
    comm.server(12002, client_signal);

    last_save=current_time;
    dbg_state("Finished initialize");
    while (!stop) {
        current_time=time(NULL);
        comm.poll();
        tls_comm.poll();
        if (current_time>last_save+600) {
            dbg_data("time reached to save the internal data of the server");
            save_all(config_file);
            last_save=current_time;
        }
        usleep(20000);
    }
    save_all(config_file);
}

/// Set the log level

void central::log_level(std::string level) {
   dbg_level(toint(level), 0);
}

/// Set the log file

void central::log_file(std::string file) {
   dbg_init(file.c_str());
}

/// Set the game data directory

void central::game_directory(std::string dir) {
    game_dir=dir;
}

/// Argument to program with handler routine

#define arg_sig(t,a,s) {sigc::signal<void, std::string> s##_signal; \
    s##_signal.connect(sigc::mem_fun(central_data, &central::s)); \
    arg_add(t, "", "", a, s##_signal);}

/// Main routine for the Central server

int main(int arg, char *arv[]) {
    dbg_level(DEBUG_STATE, 0);
    dbg_init("/var/log/moros_central.log");
    dbg_state("Initializing");
    signal(SIGINT, sig_int);
    signal(SIGTERM, sig_int);
    signal(SIGHUP, sig_reload);
    central central_data;
    central_data.config();

    sigc::signal<void, std::string> dummy;
    arg_add(ARG_PROGRAM, "", "", "moros_central", dummy);
    arg_add(ARG_VERSION, "", "", "0.1", dummy);
    arg_add(ARG_DATE, "", "", "April 15 2007", dummy);
    arg_add(ARG_COPYRIGHT, "", "", "Copyright (C) 2007, Jurjen Stellingwerff <jurjen@stwerff.xs4all.nl>", dummy);
    arg_add(ARG_COPYRIGHT, "", "", "This is free software.  You may redistribute copies of it under the terms of", dummy);
    arg_add(ARG_COPYRIGHT, "", "", "the GNU General Public License <http://www.gnu.org/licenses/gpl.html>.", dummy);
    arg_add(ARG_COPYRIGHT, "", "", "There is NO WARRANTY, to the extent permitted by law.", dummy);
    arg_add(ARG_USAGE, "", "", "[OPTION]...", dummy);
    arg_add(ARG_DESCR, "", "*", "User accounts and central game repository for moros", dummy);
    arg_sig("P", "Fork the process to the background leave the process id in the give pid file.", do_fork);
    arg_extra("pid_file", "FILE");
    arg_sig("S", "Session id of the chat server when it is not local.", chat_session);
    arg_extra("chat_session", "ID");
    arg_sig("L", "Logfile location, default: /var/log/moros_central.log", log_file);
    arg_extra("log_file", "FILE");
    arg_sig("l", "Loglevel 0-Only errors, 1-state info (default), 2-comm, 3-data, 4-calls", log_level);
    arg_extra("log_level", "LEVEL");
    arg_sig("d", "Game data directory, default: /var/lib/moros", game_directory);
    arg_extra("game_dir", "DIR");
    arg_add(ARG_BUGS, "", "", "info@moroshq.org", dummy);
    arg_parse(arg, arv);
    central_data.main_loop();
    return 0;
}
